// Tableau de progression : maîtrise par concept/semaine, activité, série, statistiques. import { NextResponse } from "next/server"; import { apiError, requireEnrollment } from "@/lib/api.ts"; import { requireUser } from "@/lib/auth/session.ts"; import { all, get } from "@/lib/db/index.ts"; import { masteryForCourse, LEVEL_LABELS } from "@/lib/learning/mastery.ts"; import { recommendations } from "@/lib/learning/recommend.ts"; import { normalizeCourse } from "@/lib/learning/helpers.ts"; export async function GET(_req: Request, ctx: { params: Promise<{ course: string }> }) { try { const user = await requireUser(); const course = normalizeCourse((await ctx.params).course); requireEnrollment(user.id, course); const mastery = masteryForCourse(user.id, course); const practiced = mastery.filter((m) => m.observations > 0); const globalScore = mastery.length ? mastery.reduce((s, m) => s + m.score * m.importance, 0) / mastery.reduce((s, m) => s + m.importance, 0) : 0; // Par semaine const weeks = new Map(); for (const m of mastery) { const w = m.week ?? 0; const e = weeks.get(w) ?? { score: 0, n: 0 }; e.score += m.score; e.n++; weeks.set(w, e); } // Série de jours actifs const days = all<{ d: string }>( "SELECT DISTINCT date(created_at) as d FROM activity_log WHERE user_id = ? ORDER BY d DESC LIMIT 60", user.id ).map((r) => r.d); let streak = 0; const today = new Date(); for (let i = 0; i < 60; i++) { const d = new Date(today); d.setDate(d.getDate() - i); const iso = d.toISOString().slice(0, 10); if (days.includes(iso)) streak++; else if (i > 0) break; // aujourd'hui pas encore actif ne casse pas la série d'hier } const stats = { cardsReviewed: get<{ n: number }>( `SELECT COUNT(*) as n FROM review_log rl JOIN flashcards f ON f.id = rl.card_id WHERE rl.user_id = ? AND f.course_code = ?`, user.id, course )?.n ?? 0, quizAnswered: get<{ n: number }>( `SELECT COUNT(*) as n FROM quiz_answers qa JOIN quiz_sessions qs ON qs.id = qa.session_id WHERE qs.user_id = ? AND qs.course_code = ?`, user.id, course )?.n ?? 0, quizAccuracy: get<{ p: number }>( `SELECT ROUND(AVG(qa.correct)*100) as p FROM quiz_answers qa JOIN quiz_sessions qs ON qs.id = qa.session_id WHERE qs.user_id = ? AND qs.course_code = ?`, user.id, course )?.p ?? null, examAttempts: get<{ n: number }>( `SELECT COUNT(*) as n FROM exam_attempts ea JOIN mock_exams me ON me.id = ea.exam_id WHERE ea.user_id = ? AND me.course_code = ? AND ea.finished_at IS NOT NULL`, user.id, course )?.n ?? 0, studyMinutes: Math.round((get<{ s: number }>( "SELECT COALESCE(SUM(duration_s),0) as s FROM activity_log WHERE user_id = ? AND course_code = ?", user.id, course )?.s ?? 0) / 60), errorsToReview: get<{ n: number }>( "SELECT COUNT(*) as n FROM error_notebook WHERE user_id = ? AND course_code = ? AND status = 'a-revoir'", user.id, course )?.n ?? 0, cardsDue: get<{ n: number }>( `SELECT COUNT(*) as n FROM card_states cs JOIN flashcards f ON f.id = cs.card_id WHERE cs.user_id = ? AND f.course_code = ? AND cs.suspended = 0 AND cs.due_at <= datetime('now')`, user.id, course )?.n ?? 0, }; const recent = all( `SELECT kind, meta, created_at FROM activity_log WHERE user_id = ? AND course_code = ? ORDER BY id DESC LIMIT 10`, user.id, course ); return NextResponse.json({ globalScore, levels: LEVEL_LABELS, concepts: mastery, practicedCount: practiced.length, weekScores: [...weeks.entries()].map(([week, v]) => ({ week, score: v.score / v.n })).sort((a, b) => a.week - b.week), streak, stats, recent, recommendations: recommendations(user.id, course), }); } catch (e) { return apiError(e); } }